using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Input.Touch;
using Perovich.GameObjects;
using Perovich.GameObjects.Helpers;
using System.Collections.ObjectModel;
using Perovich.GameObjects.Interfaces;

namespace Perovich.GameObjects.TwoDimensional
{

    /// <summary>
    /// The <see cref="InteractiveObject2D"/> class is a <see cref="TextureSpriteObject2D"/> that supports collision detection with other <see cref="TextureSpriteObject2D"/>s.
    /// </summary>
    /// <remarks>
    /// The <see cref="InteractiveObject2D.Collided"/> event is deterministicly tirggered after all update events have been processed in the game loop.
    /// </remarks>
    public class InteractiveObject2D : TextureSpriteObject2D, ICollidable
    {
        /// <summary>
        /// Gets or sets a value indicating whether this object generates mouse click events.
        /// </summary>
        public bool GeneratesMouseClicks
        {
            get;
            set;
        }

        /// <summary>
        /// Gets the <see cref="BoundingBox"/> used for collision detection.
        /// </summary>
        public BoundingBox CollisionBox
        {
            get
            {
                Vector2 pos = Position - Origin;
                return new BoundingBox(new Vector3(pos.X, pos.Y, 0), new Vector3(pos.X + Texture.Width, pos.Y + Texture.Height, 0));
            }
        }

        public dynamic CollisionSolid
        {
            get
            {
                return CollisionBox;
            }
        }

        /// <summary>
        /// Gets or sets the tolerance for tap events.
        /// </summary>
        public float TapTolerance
        {
            get;
            set;
        }

        private List<Object> collisionTargets = new List<object>();


        /// <summary>
        /// Gets a mutable list of objects this <see cref="InteractiveObject2D"/> collides with and <see cref="BoundingBox"/>es it must remain in.
        /// </summary>
        /// <value>A <see cref="Collection{Object}"/> containing all the <see cref="InteractiveObject2D"/>s this <see cref="InteractiveObject2D"/> collides with and <see cref="BoundingBox"/>es it must remain in.</value>
        protected Collection<Object> CollisionTargetsMutable
        {
            get { return new Collection<object>(collisionTargets); }
        }


        /// <summary>
        /// Gets the list of objects this <see cref="InteractiveObject2D"/> collides with and <see cref="BoundingBox"/>es it must remain in.
        /// </summary>
        /// <value>A <see cref="ReadOnlyCollection{Object}"/> containing all the <see cref="InteractiveObject2D"/>s this <see cref="InteractiveObject2D"/> collides with and <see cref="BoundingBox"/>es it must remain in.</value>
        public ReadOnlyCollection<object> CollisionTargets
        {
            get
            {
                return new ReadOnlyCollection<object>(collisionTargets);
            }
        }


        

        /// <summary>
        /// Triggered whenever the <see cref="InteractiveObject2D"/> gets clicked.
        /// </summary>
        public event EventHandler Clicked;

        /// <summary>
        /// Triggered whenever a collision is detected or the <see cref="InteractiveObject2D"/> goes outside one of the <see cref="BoundingBox"/>es.
        /// </summary>
        public event EventHandler<CollisionEventArgs> Collided;

        /// <summary>
        /// A default collision handler that produces square bouncing.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        public static void DefaultBounceHandler(object sender, CollisionEventArgs e)
        {
            if (e == null)
                throw new ArgumentNullException("e");
            BoundedObject2D me = sender as BoundedObject2D;
            BoundedObject2D them = e.Other as BoundedObject2D;
            if (them == null) return;
            if (Math.Abs(me.Position.X - them.Position.X) > Math.Abs(me.Position.Y - them.Position.Y))
            {
                if (me.Position.X < them.Position.X)
                {
                    if (me.Velocity.X > 0)
                        me.Velocity = new Vector2(-me.Velocity.X, me.Velocity.Y);
                    if (them.Velocity.X < 0)
                        them.Velocity = new Vector2(-them.Velocity.X, them.Velocity.Y);
                }
                else if (me.Position.X > them.Position.X)
                {
                    if (me.Velocity.X < 0)
                        me.Velocity = new Vector2(-me.Velocity.X, me.Velocity.Y);
                    if (them.Velocity.X > 0)
                        them.Velocity = new Vector2(-them.Velocity.X, them.Velocity.Y);
                }
            }
            else if (Math.Abs(me.Position.X - them.Position.X) < Math.Abs(me.Position.Y - them.Position.Y))
            {
                if (me.Position.Y < them.Position.Y)
                {
                    if (me.Velocity.Y > 0)
                        me.Velocity = new Vector2(me.Velocity.X, -me.Velocity.Y);
                    if (them.Velocity.Y < 0)
                        them.Velocity = new Vector2(them.Velocity.X, -them.Velocity.Y);
                }
                else if (me.Position.Y > them.Position.Y)
                {
                    if (me.Velocity.Y < 0)
                        me.Velocity = new Vector2(me.Velocity.X, -me.Velocity.Y);
                    if (them.Velocity.Y > 0)
                        them.Velocity = new Vector2(them.Velocity.X, -them.Velocity.Y);
                }
            }
            else
            {
                if (me.Position.X < them.Position.X)
                {
                    if (me.Velocity.X > 0)
                        me.Velocity = new Vector2(-me.Velocity.X, me.Velocity.Y);
                    if (them.Velocity.X < 0)
                        them.Velocity = new Vector2(-them.Velocity.X, them.Velocity.Y);
                }
                else if (me.Position.X > them.Position.X)
                {
                    if (me.Velocity.X < 0)
                        me.Velocity = new Vector2(-me.Velocity.X, me.Velocity.Y);
                    if (them.Velocity.X > 0)
                        them.Velocity = new Vector2(-them.Velocity.X, them.Velocity.Y);
                }
                if (me.Position.Y < them.Position.Y)
                {
                    if (me.Velocity.Y > 0)
                        me.Velocity = new Vector2(me.Velocity.X, -me.Velocity.Y);
                    if (them.Velocity.Y < 0)
                        them.Velocity = new Vector2(them.Velocity.X, -them.Velocity.Y);
                }
                else if (me.Position.Y > them.Position.Y)
                {
                    if (me.Velocity.Y < 0)
                        me.Velocity = new Vector2(me.Velocity.X, -me.Velocity.Y);
                    if (them.Velocity.Y > 0)
                        them.Velocity = new Vector2(them.Velocity.X, -them.Velocity.Y);
                }
            }
        }


        /// <summary>
        /// Constructs a new instance of the <see cref="InteractiveObject2D"/> class.
        /// </summary>
        /// <param name="game"><see cref="Perovich.GameObjects.Interfaces.ICanHoldComponents"/> instance that this <see cref="InteractiveObject2D"/> will be constructed for.</param>
        public InteractiveObject2D(Interfaces.ICanHoldComponents game)
            : base(game)
        {
            TapTolerance = 0;
            collisionTargets = new List<object>();
            GestureHelper.Initialize(game);
            GestureHelper.AddCallback(GestureType.Tap, GHTouchCallback);
            IsPreciseCollisions = false;
        }




        /// <summary>
        /// Adds one or more <see cref="InteractiveObject2D"/>s to this instances <see cref="CollisionTargets"/> collection.
        /// </summary>
        /// <param name="targets">The <see cref="InteractiveObject2D"/>s to add.</param>
        public void AddCollisionTargets(params InteractiveObject2D[] targets)
        {
            if (targets == null)
                throw new ArgumentNullException("targets");
            foreach (InteractiveObject2D obj in targets)
            {
                if ((!obj.CollisionTargets.Contains(this))&& (obj != this))
                    collisionTargets.Add(obj);
            }
        }

        /// <summary>
        /// Adds one or more <see cref="InteractiveObject2D"/>s to this instances <see cref="CollisionTargets"/> collection.
        /// </summary>
        /// <param name="targets">The <see cref="InteractiveObject2D"/>s to add.</param>
        public void AddCollisionTargets(IEnumerable<InteractiveObject2D> targets)
        {
            if (targets == null)
                throw new ArgumentNullException("targets");
            foreach (InteractiveObject2D obj in targets)
            {
                if ((!obj.CollisionTargets.Contains(this)) && (obj != this))
                    collisionTargets.Add(obj);
            }
        }

        /// <summary>
        /// Adds one or more <see cref="BoundingBox"/>es to this instances <see cref="CollisionTargets"/> collection.
        /// </summary>
        /// <param name="targets">The <see cref="BoundingBox"/>es to add.</param>
        public void AddCollisionTargets(params BoundingBox[] targets)
        {
            if (targets == null)
                throw new ArgumentNullException("targets");
            foreach (BoundingBox bb in targets)
            {
                collisionTargets.Add(bb);
            }
        }

        /// <summary>
        /// Adds one or more <see cref="BoundingBox"/>es to this instances <see cref="CollisionTargets"/> collection.
        /// </summary>
        /// <param name="targets">The <see cref="BoundingBox"/>es to add.</param>
        public void AddCollisionTargets(IEnumerable<BoundingBox> targets)
        {
            if (targets == null)
                throw new ArgumentNullException("targets");
            foreach (BoundingBox bb in targets)
            {
                collisionTargets.Add(bb);
            }
        }


        void ICollidable.CheckCollisions()
        {
            CheckCollisionsCore();
        }

        /// <summary>
        /// Allows the <see cref="InteractiveObject2D"/> to check for collisions.
        /// </summary>
        protected virtual void CheckCollisionsCore()
        {
            if (Active)
            {
                foreach (object other in CollisionTargets)
                {
                    InteractiveObject2D inter = other as InteractiveObject2D;
                    if (inter != null)
                    {
                        if (CollisionBox.Intersects(inter.CollisionBox))
                        {
                            SignalCollision(inter);
                        }
                    }
                    else if (other is BoundingBox)
                    {
                        BoundingBox merged = BoundingBox.CreateMerged(CollisionBox, (BoundingBox)other);
                        if (merged != (BoundingBox)other)
                        {
                            SignalCollision((BoundingBox)other);
                        }
                    }
                }
            }
        }


        private void GHTouchCallback(GestureSample gs)
        {
            if (Active)
            {
                Vector2 position = gs.Position;
                BoundingBox TouchBB = new BoundingBox(new Vector3(position.X - TapTolerance, position.Y - TapTolerance, 0),
                                                  new Vector3(position.X + TapTolerance, position.Y + TapTolerance, 0));

                if (CollisionBox.Intersects(TouchBB))
                {
                    SignalClick();
                }
            }
        }


        /// <summary>
        /// Gets or sets a value indicating whether this object checks pixel perfect collisions.
        /// </summary>
        public bool IsPreciseCollisions
        {
            get;
            set;
        }

        /// <summary>
        /// Called when a collision is detected. Deriving classes can override this method to add additional collision handling.
        /// </summary>
        /// <param name="other">The <see cref="InteractiveObject2D"/> that collided with this object.</param>
        protected void SignalCollision(InteractiveObject2D other)
        {
            if ((!IsPreciseCollisions) || PixelPerfectCollision(other))
            {
                if (Collided != null)
                {
                    Collided(this, new CollisionEventArgs(other));
                }
            }
        }

        private bool PixelPerfectCollision(InteractiveObject2D other)
        {
            int top = (int)Math.Max(this.CollisionBox.Top(), other.CollisionBox.Top());
            int bottom = (int)Math.Min(this.CollisionBox.Bottom(), other.CollisionBox.Bottom());
            int left = (int)Math.Max(this.CollisionBox.Left(), other.CollisionBox.Left());
            int right = (int)Math.Min(this.CollisionBox.Right(), other.CollisionBox.Right());

            var dataA = Textures.GetCollisionData(this.Texture);
            var dataB = Textures.GetCollisionData(other.Texture);

            // Check every point within the intersection bounds
            for (int y = top; y < bottom; y++)
            {
                for (int x = left; x < right; x++)
                {
                    // Get the color of both pixels at this point
                    Color colorA = dataA[(x - this.CollisionBox.Left()) +
                                (y - this.CollisionBox.Top()) * this.CollisionBox.Width()];
                    Color colorB = dataB[(x - other.CollisionBox.Left()) +
                                (y - other.CollisionBox.Top()) * other.CollisionBox.Width()];

                    // If both pixels are not completely transparent,
                    if (colorA.A != 0 && colorB.A != 0)
                    {
                        // then an intersection has been found
                        return true;
                    }
                }
            }

            // No intersection found
            return false;

        }

        /// <summary>
        /// Called when a <see cref="BoundingBox"/> is moved out of. Deriving classes can override this method to add additional handling.
        /// </summary>
        /// <param name="other">The <see cref="BoundingBox"/> that this object moved out of.</param>
        protected void SignalCollision(BoundingBox other)
        {
            if ((!IsPreciseCollisions) || PixelPerfectBounding(other))
            {
                if (Collided != null)
                {
                    Collided(this, new CollisionEventArgs(other));
                }
            }
        }

        private bool PixelPerfectBounding(BoundingBox other)
        {
            int t2 = (int)Math.Max(this.CollisionBox.Top(), other.Top());
            int b2 = (int)Math.Min(this.CollisionBox.Bottom(), other.Bottom());
            int l2 = (int)Math.Max(this.CollisionBox.Left(), other.Left());
            int r2 = (int)Math.Min(this.CollisionBox.Right(), other.Right());
            
            int t1 = this.CollisionBox.Top();
            int b1 = this.CollisionBox.Bottom();
            int l1 = this.CollisionBox.Left();
            int r1 = this.CollisionBox.Right();




            int left = (l1 < l2) ? l1 : r2;
            int right = (r1 > r2) ? r1 : l2;
            int top = (t1 < t2) ? t1 : b2;
            int bottom = (b1 > b2) ? b1 : t2;
            if ((t1 == t2) && (b1 == b2) && (l1 == l2) && (r1 == r2))
            {
                bottom = top;
                right = left;
            }
            

            var dataA = Textures.GetCollisionData(this.Texture);

            // Check every point within the intersection bounds

            //Check the top and bottom
            for (int y = top; y < bottom; y++)
            {
                for (int x = l1; x < r1; x++)
                {
                    // Get the color of both pixels at this point
                    Color colorA = dataA[(x - this.CollisionBox.Left()) +
                                (y - this.CollisionBox.Top()) * this.Texture.Width];

                    // If both pixels are not completely transparent,
                    if (colorA.A != 0)
                    {
                        // then an intersection has been found
                        return true;
                    }
                }
            }


            //Check the left and right
            for (int y = t1; y < b2; y++)
            {
                for (int x = left; x < right; x++)
                {
                    // Get the color of both pixels at this point
                    Color colorA = dataA[(x - this.CollisionBox.Left()) +
                                (y - this.CollisionBox.Top()) * this.Texture.Width];

                    // If both pixels are not completely transparent,
                    if (colorA.A != 0)
                    {
                        // then an intersection has been found
                        return true;
                    }
                }
            }

            // No intersection found
            return false;
        }

        private void SignalClick()
        {
            if (Clicked != null)
            {
                Clicked(this, new EventArgs());
            }
        }

    }
}
